package com.compomics.cellmotionsimulator;


import com.compomics.cellmotionsimulator.components.Cell;
import com.compomics.cellmotionsimulator.gui.CellMotionFrame;

import java.awt.*;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;

/**
 * Created with IntelliJ IDEA.
 * User: Lennart
 * Date: 28/10/14
 * Time: 09:22
 * To change this template use File | Settings | File Templates.
 */
public class CellMotionSimulator {

    private int iNumberOfCells = 0;
    private int iNumberOfSteps = 0;
    private Dimension iWorldSize = null;

    private Collection<Cell> iCells = null;


    /**
     * Constructor for the simulator, taking the the size of the world, the number of steps,
     * and the number of cells,
     *
     * @param aWorldSize    Dimension with the size of the world (in pixels)
     * @param aNumberOfSteps    int with the number of steps to simulate
     * @param aNumberOfCells    int with the number of cells to simulate
     */
    public CellMotionSimulator(Dimension aWorldSize, int aNumberOfSteps, int aNumberOfCells) {

        // Define the size of the world (in pixels)
        this.setWorldSize(aWorldSize);

        // Define the number of steps
        this.setNumberOfSteps(aNumberOfSteps);

        // Define the number of cells
        this.setNumberOfCells(aNumberOfCells);
    }

    public void startSimulation() {
        final CellMotionFrame cmf = new CellMotionFrame(this);

        Runnable run = new Runnable() {
            public void run() {
                // First we cycle through each step.
                for (int i = 0; i < iNumberOfSteps; i++) {
                    // For each step, we have to move each cell
                    for (Iterator<Cell> iterator = iCells.iterator(); iterator.hasNext(); ) {
                        Cell cell = iterator.next();

                        // For each cell, we calculate the turning angle.
                        double turningAngleInRadians = determineCellTurningAngle();

                        // Now we calculate the distance of movement
                        int distanceOfMovementInPixels = determineDistanceOfMovement(turningAngleInRadians);

                        // Correct the turning angle to reflect the angle compared to the
                        // last turning angle.
                        double recalibratedTurningAngleInRadians = cell.getPreviousRecalibratedAngle()+turningAngleInRadians;

                        // Add the turning angle.
                        cell.addFacingAngle(turningAngleInRadians);

                        // Update the last recalibrated turning angle (to recalibrate the next turning angle)
                        cell.setPreviousRecalibratedAngle(recalibratedTurningAngleInRadians);

                        // Now calculate the destination location of the cell.
                        Point endPoint = calculateEndPoint(cell.getStep(i), recalibratedTurningAngleInRadians, distanceOfMovementInPixels);

                        // Add the destination step to the cells step history.
                        cell.addStep(endPoint);
                        cmf.repaint();
                        try {
                            Thread.sleep(5);
                        } catch(InterruptedException ie) {
                            //Never mind.
                        }
                    }
                }
                System.out.println("\nFinished simulation!\n\nPrinting results...\n");
                try {
                    BufferedWriter bw = new BufferedWriter(new FileWriter("c:/Users/Lennart/Desktop/cellmotionpaths.csv"));
                    // Now print out the steps for each cell.
                    // First the header line (one column per cell).
                    bw.write("Cell\t");
                    for (Iterator<Cell> iterator = iCells.iterator(); iterator.hasNext(); ) {
                        Cell cell = iterator.next();
                        bw.write(cell.getCellName() + "_X");
                        bw.write("\t");
                        bw.write(cell.getCellName() + "_Y");
                        bw.write("\t");
                        bw.write(cell.getCellName() + "_angle_(rad)");
                        bw.write("\t");
                        bw.write(cell.getCellName() + "_distance_(pxl)");
                        if (iterator.hasNext()) {
                            bw.write("\t");
                        } else {
                            bw.write(System.lineSeparator());
                        }
                    }
                    // Now write out each step for each cell.
                    for (int i = 0; i < (iNumberOfSteps + 1); i++) {
                        bw.write("Step_" + i + "\t");
                        for (Iterator<Cell> iterator = iCells.iterator(); iterator.hasNext(); ) {
                            Cell cell = iterator.next();
                            Point step = cell.getStep(i);
                            double distance = 0.0;
                            if(i>0) {
                                Point previousStep = cell.getStep(i - 1);
                                distance = Math.sqrt(Math.pow(step.getX()-previousStep.getX(), 2) + Math.pow(step.getY()-previousStep.getY(), 2));
                            }
                            bw.write(step.getX() + "\t" + step.getY() + "\t" + cell.getFacingAngle(i) + "\t" + distance);
                            if (iterator.hasNext()) {
                                bw.write("\t");
                            } else {
                                bw.write(System.lineSeparator());
                            }
                        }
                    }
                    bw.flush();
                    bw.close();
                    System.out.println("Results printed.\n\n");
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
        };

        Thread thread = new Thread(run);
        thread.setDaemon(true);
        thread.start();

        cmf.setVisible(true);
    }

    public int getNumberOfCells() {
        return iNumberOfCells;
    }

    public void setNumberOfCells(int aNumberOfCells) {
        this.iNumberOfCells = aNumberOfCells;
        iCells = new Vector<Cell>(iNumberOfCells);
        for (int i = 0; i < iNumberOfCells; i++) {
            iCells.add(new Cell( (i+1), getNumberOfSteps(), getStartingPosition()));
        }
    }

    public int getNumberOfSteps() {
        return iNumberOfSteps;
    }

    public void setNumberOfSteps(int aNumberOfSteps) {
        this.iNumberOfSteps = aNumberOfSteps;
    }

    public Dimension getWorldSize() {
        return iWorldSize;
    }

    public void setWorldSize(Dimension aWorldSize) {
        this.iWorldSize = aWorldSize;
    }

    public Collection<Cell> getCells() {
        return iCells;
    }


    ////// MAIN METHOD



    public static void main(String[] args) {
        CellMotionSimulator cms = new CellMotionSimulator(new Dimension(900, 900), 300, 20);
        cms.startSimulation();

    }



////// PRIVATE FUNCTIONS


    /**
     * This method determines the turning angle in radians.
     *
     * @return double with the turning angle in radians.
     */
    private double determineCellTurningAngle() {
        double result = 0.0;
        double die = Math.random();
        //double die = 0.5;
        //double die = Math.random()*0.84;
        if(die < 0.15) {
            result = Math.random() * Math.PI * 2;
            if(result < Math.PI*3/2 && result > Math.PI/2) {
                System.out.println(" **** ZIEDETWEL!");
            }
        } else if(die >= 0.15 && die < 0.25) {
            result = Math.PI/2;
            if(Math.random() < 0.5) {
                result = (2*Math.PI)-result;
            }
            return result;
        } else {
            result = (Math.random() * 45) * Math.PI/180;
            if(Math.random() < 0.5) {
                result = (2*Math.PI)-result;
            }
        }

        return result;
    }

    /**
     * This method returns the distance of the movement in pixels.
     *
     * @return  int with the distance of the movement in pixels.
     */
    private int determineDistanceOfMovement(double aTurningAngleInRadians) {

        double proportionOfMaximumTurningAngle = aTurningAngleInRadians/Math.PI;
        int distance = (int)Math.round(Math.abs(1-proportionOfMaximumTurningAngle)*100);

        // int distance = new BigDecimal((Math.random()*10)+10).setScale(0, BigDecimal.ROUND_HALF_UP).intValue();

        return distance;
    }

    private Point calculateEndPoint(Point aStartingPoint, double aTurningAngle, int aDistance) {
        // The new position is calculated based on the cos and sin of the turning angle (for X and Y components,
        // respectively), and the total distance of movement.
        int new_x = (int)aStartingPoint.getX() + new BigDecimal(Math.cos(aTurningAngle) * aDistance).setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
        int new_y = (int)aStartingPoint.getY() + new BigDecimal(Math.sin(aTurningAngle) * aDistance).setScale(0, BigDecimal.ROUND_HALF_UP).intValue();

        // Return the new point.
        return new Point(new_x, new_y);
    }

    private Point getStartingPosition() {
        int x = new BigDecimal(Math.random()*iWorldSize.getWidth()).setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
        int y = new BigDecimal(Math.random()*iWorldSize.getHeight()).setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
        Point startingPoint = new Point(x, y);
        return startingPoint;
    }
}
